using System;
using System.Diagnostics;
using System.IO;
using System.Threading;
using System.Web;
using System.Xml;
using BoxBinary.AspectCache.Core.Broadcast;
//using BoxBinary.Logging;
using BoxBinary.AspectCache.Core.ErrorLogging;
using BoxBinary.AspectCache.Core.Helpers;
using BoxBinary.AspectCache.Core.Logging;
using BoxBinary.AspectCache.Core.PolicyDefinition;
using Path = BoxBinary.AspectCache.Core.Helpers.Path;

namespace BoxBinary.AspectCache.Core.ReplicationSink
{
    public class SinkWatcher : IHttpModule
    {
        #region Instance methods

        public void Dispose()
        {
            // Don't dispose items here as they are static and used by multiple requests
            //if (_stateSinkMonitor != null)
            //    _stateSinkMonitor.Dispose();
            //_stateSinkMonitor = null;
        }

        public void Init(HttpApplication context)
        {
            if (!_isInitialisedInThisAppDomain)
            {
                if (Configuration.Current.EnableCacheStateReplicationSink)
                {
                    _errorLogger =
                        new EmailErrorLogger();
                    //TODO: This is temporary
                    ((EmailErrorLogger) _errorLogger).EmailToAddress = "alex.norcliffe@condenast.co.uk";

                    // _errorLogger = CondeNetUK.EktronCommon.Business.Components.VaderPageContext.Current.DefaultErrorLogger;
                    // Setup file system watcher to keep track of serialized cache items
                    InitReplicationSinkWatcher();
                    cleanerThread = new SinkMonitorThreadStart(new SinkMonitorThread());
                    _isInitialisedInThisAppDomain = true;
                }
            }
        }

        #endregion

        private static readonly object traceLogLockObject = new object();
        private static FileSystemWatcher __stateSinkMonitor;
        private static ILogProvider _errorLogger;
        private static bool _isInitialisedInThisAppDomain;
        private static SinkMonitorThreadStart cleanerThread;
        private static string TraceLogLocation = @"C:\Temp\ReplicationTraceLog.traceLog";

        protected static FileSystemWatcher WatcherInstance
        {
            get
            {
                InitReplicationSinkWatcher();
                return __stateSinkMonitor;
            }
            set { __stateSinkMonitor = value; }
        }

        private static void InitReplicationSinkWatcher()
        {
            if (__stateSinkMonitor == null)
                __stateSinkMonitor = GetFileWatcher();
        }

        private static FileSystemWatcher GetFileWatcher()
        {
            try
            {
                if (!Directory.Exists(SinkManager.GetReplicationSinkRootedPath()))
                    Directory.CreateDirectory(SinkManager.GetReplicationSinkRootedPath());

                var newWatcher =
                    new FileSystemWatcher(SinkManager.GetReplicationSinkRootedPath());

                // Only listen to events involving the size of the folder changing.
                newWatcher.NotifyFilter = NotifyFilters.Size;
                // Ensure mass amounts of changes are still responded to, trying not to raise an InternalBufferOverflowException
                newWatcher.InternalBufferSize = 500000;
                // Don't monitor subfolders
                newWatcher.IncludeSubdirectories = false;
                // Only monitor files we're bothered about
                newWatcher.Filter = string.Format("*{0}", IO.SerializationFileNameExtension);

                // Only monitor file creation events. Files will never be changed as part of the BroadcastEvent system,
                // as they are just a message queue
                newWatcher.Created += OnReplicationSinkWatcherChanged;

                newWatcher.Error += OnReplicationSinkWatcherError;
                newWatcher.EnableRaisingEvents = true;

                return newWatcher;
            }
            catch (Exception e)
            {
                LogError(new Exception("An error occurred during setup of the monitoring of the CacheStateSink", e));
            }
            return null;
        }

        /// <summary>
        /// Quick and dirty way of getting a central file logger without
        /// monitoring thread-safe access.
        /// </summary>
        /// <remarks>Compiled with a ConditionalAttribute to ensure no logging takes place in a release build</remarks>
        [Conditional("DEBUG")]
        internal static void ConditionalDebug_WriteToTraceLog(string eventCategory, string message)
        {
            if (Configuration.Current.EnableReplicationSinkMonitorLog)
            {
                bool success = false;
                int writeAttempts = 0;

                while (success == false && writeAttempts < 5)
                {
                    try
                    {
                        writeAttempts += 1;
                        lock (traceLogLockObject)
                        {
                            StreamWriter sr;
                            var fInfo = new FileInfo(TraceLogLocation);

                            if (!fInfo.Exists)
                                sr = new StreamWriter(fInfo.Create());
                            else
                                sr = fInfo.AppendText();

                            using (sr)
                            {
                                sr.WriteLine(
                                                string.Format("{0}  {1} {2} Event: {3}. \t{4}",
                                                              XmlConvert.ToString(DateTime.Now,
                                                                                  XmlDateTimeSerializationMode.Local),
                                                              HttpRuntime.AppDomainAppId,
                                                              Thread.CurrentThread.ManagedThreadId,
                                                              eventCategory,
                                                              message.Replace("\n\r\n\r", "\n\r").Replace("\n\r",
                                                                                                          "\t\n\r")));
                                sr.Flush();
                                sr.Close();
                            }
                            success = true;
                        }
                    }
                    catch
                    {
                        // Ignore error 
                        if (writeAttempts < 5)
                            // Sleep to wait for another thread to release a file lock,
                            Thread.Sleep(200);
                    }
                }
            }
        }

        private static void LogError(Exception e)
        {
            //if (_errorLogger == null)
            //{
            //    if (HttpContext.Current != null)
            //    {
            //        try
            //        {
            //            _errorLogger = CondeNetUK.EktronCommon.Business.Components.VaderPageContext.Current.DefaultErrorLogger;
            //        }
            //        catch
            //        {
            //            /* Ignore error */
            //        }
            //    }
            //}
            //if (_errorLogger != null)
            //{
            ErrorHelper.LogError(e, true);
            //}

            // Also write to trace log
            ConditionalDebug_WriteToTraceLog("Error!", e.ToString());
        }

        internal static void RestartMonitoring()
        {
            WatcherInstance = null;
            InitReplicationSinkWatcher();
        }

        private static void OnReplicationSinkWatcherError(object sender, ErrorEventArgs eArgs)
        {
            bool logError = true;
            bool delayRestart = false;

            string msg =
                string.Format(
                                 "An error occurred ({0}) during monitoring of the CacheStateSink. A restart attempt will be made. ",
                                 eArgs.GetException().Message);

            if (eArgs.GetException() is InternalBufferOverflowException)
            {
                //msg += string.Format("NB More than {0} changes happened to {1} and the monitor gave up.", WatcherInstance.InternalBufferSize, WatcherInstance.Path);
                logError = false;
                // Delay the restart in case something crazy is happening with the folder
                delayRestart = true;
            }

            if (logError)
            {
                LogError(
                            new Exception(
                                msg,
                                eArgs.GetException()));
            }

            if (delayRestart)
            {
                // Pause for 10 seconds
                Thread.Sleep(10000);
            }
            RestartMonitoring();
        }

        private static void OnReplicationSinkWatcherChanged(object sender, FileSystemEventArgs eArgs)
        {
            try
            {
                ConditionalDebug_WriteToTraceLog("FSW " + eArgs.ChangeType, eArgs.Name);

                switch (eArgs.ChangeType)
                {
                    case WatcherChangeTypes.Created:
                        // Only respond to the event if this item was created on another machine
                        // otherwise we'd be constantly responding to files created on the same machine
                        if (!Helpers.Path.IsSerializedPathFromThisAppDomain(eArgs.Name))
                        {
                            string serializedFileName = eArgs.Name;
                            ConditionalDebug_WriteToTraceLog("File is from another appdomain", "Processing...");
                            LoadCacheEntryFromDisk(serializedFileName);
                        }
                        break;
                }
            }
            catch (Exception e)
            {
                LogError(
                            new Exception(
                                string.Format(
                                                 "An error ('{0}') occurred responding to changes in the CacheStateSink folder, at path {1}",
                                                 e.Message,
                                                 eArgs.FullPath), e));
                RestartMonitoring();
            }
        }

        protected static void LoadCacheEntryFromDisk(string serializedFileName)
        {
            if (HttpRuntime.Cache != null)
            {
                // Load the item into the cache collection on this machine
                BroadcastEvent broadcastEventHandler = SinkManager.Deserialize(serializedFileName);
                if (broadcastEventHandler != null)
                {
                    // Ensure that we haven't just received an event which is older than
                    // our current cache entry
                    if (broadcastEventHandler.Package != null)
                    {
                        WebPolicyBoundCachePackage existingPolicyFromThisMachine =
                            CacheManager.RetrievePolicy(broadcastEventHandler.Package.Key);
                        if (existingPolicyFromThisMachine == null ||
                            existingPolicyFromThisMachine.ItemCachedDateStamp < broadcastEventHandler.DateStamp)
                        {
                            // Respond to the event raised
                            switch (broadcastEventHandler.Reason)
                            {
                                default:
                                case BroadcastEventReason.ItemDeleted:
                                case BroadcastEventReason.ItemUpdatedNotBroadcastable:
                                    lock (CacheManager.CacheRemovedDelegateLockObject)
                                    {
                                        ConditionalDebug_WriteToTraceLog(
                                                                            "Policy" + broadcastEventHandler.Reason,
                                                                            "Deleting " +
                                                                            broadcastEventHandler.Package.Key);

                                        CacheManager.CacheRemovedReason =
                                            CacheManager.CacheRemovalReason.RemovedDueToUpdate;
                                        HttpRuntime.Cache.Remove(broadcastEventHandler.Package.Key);
                                    }
                                    break;
                                case BroadcastEventReason.ItemCreated:
                                case BroadcastEventReason.ItemUpdated:
                                    // Only load items that have not expired beyond their use
                                    if (DateTime.Now < broadcastEventHandler.Package.AbsoluteExpiration)
                                    {
                                        lock (CacheManager.CacheRemovedDelegateLockObject)
                                        {
                                            if (broadcastEventHandler.Package != null &&
                                                broadcastEventHandler.Package.Item != null)
                                            {
                                                ConditionalDebug_WriteToTraceLog(
                                                                                    "Policy" +
                                                                                    broadcastEventHandler.Reason,
                                                                                    "Inserting " +
                                                                                    broadcastEventHandler.Package.Key);
                                                CacheManager.Insert(
                                                                          (WebPolicyBoundCachePackage)
                                                                          broadcastEventHandler.Package, false);
                                            }
                                        }
                                    }
                                    break;
                            }
                        }
                    }
                }
            }
        }
    }
}